#include <vector>
#include <d3d9.h>
#include <dxva2api.h>
#include "Log/SystemLog.h"
#include "VideoD3dRender.h"
#include "VideoHardDecoder.h"

// Construct the VideoHardDecoder
VideoHardDecoder::VideoHardDecoder() :
	m_pVideoRender(NULL),
	m_pWinHandle(NULL),
	m_pDecCodecCtx(NULL),
	m_ehwPixFmt(AV_PIX_FMT_NONE),
	m_phwDevCtx(NULL),
	m_bIsHaveInitDecoder(false),
	m_bDisposed(false)
{
	Initialize();
}

// Detructe the VideoHardDecoder
VideoHardDecoder::~VideoHardDecoder()
{
	Destory();
}

// Initialize the decoder
void VideoHardDecoder::Initialize()
{

}

// Destory the decoder
void VideoHardDecoder::Destory()
{
	if (!GetDisposed())
	{
		SetDisposed(true);

		// Release the decoder
		ReleaseDecoder();
	}
}

// Create video render
void VideoHardDecoder::CreateRender()
{
	SetVideoRender(new VideoD3dRender());
}

// Destory video render
void VideoHardDecoder::DestoryRender()
{
	if (GetVideoRender())
	{
		delete GetVideoRender();

		SetVideoRender(NULL);
	}
}

// Init the video render
bool VideoHardDecoder::InitVideoRender()
{
	if (GetVideoRender() == NULL)
	{
		LOG_ERROR_EX("Please create video render at first!");

		return false;
	}

	if (!GetVideoRender()->Init(GetDecCodecCtx(),GetWinHandle()))
	{
		return false;
	}

	return true;
}

// Release the decoder
void VideoHardDecoder::ReleaseDecoder()
{
	// Destory the render
	DestoryRender();

	AVCodecContext* pAvctx = GetDecCodecCtx();

	if (pAvctx)
	{
		avcodec_close(pAvctx);

		avcodec_free_context(&pAvctx);

		av_free(pAvctx);

		SetDecCodecCtx(NULL);
	}

	if (GethwDevCtx())
	{
		av_buffer_unref(&m_phwDevCtx);

		SethwDevCtx(NULL);
	}
}

// Flush the decode to ensure all the frame output
void VideoHardDecoder::Flush()
{
	int iDstSize = 0;

	DecodePacket(GetDecCodecCtx(), NULL, NULL, iDstSize);
}

// Is show data by the GPU directly
bool VideoHardDecoder::IsShowByGpu()
{
	if (GetWinHandle() == NULL)
	{
		return false;
	}

	return true;
}

// Is current system support hard decoder
bool VideoHardDecoder::IsSupportHardDecode(VideoTypeEnum eVideoType)
{
	// Find a decoder now
	AVCodec* pDecoder = NULL;

	if (eVideoType == H264_VIDEO)
	{
		pDecoder = avcodec_find_decoder(AV_CODEC_ID_H264);
	}
	else if (eVideoType == H265_VIDEO)
	{
		pDecoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
	}

	if (pDecoder == NULL)
	{
		std::string strErrorMsg = std::string("Failed to find ")
			+ av_get_media_type_string(AVMEDIA_TYPE_VIDEO)
			+ " codec\n";

		LOG_ERROR_EX(strErrorMsg);

		return false;
	}

	// Create decode context
	AVCodecContext* pAvctx = avcodec_alloc_context3(pDecoder);

	pAvctx->flags |= AV_CODEC_FLAG_LOW_DELAY;

	pAvctx->thread_count = 0;

	// Get all available device table
	AVHWDeviceType eDeviceType = AV_HWDEVICE_TYPE_NONE;

	std::vector<std::string> vDeviceTable;

	while ((eDeviceType = av_hwdevice_iterate_types(eDeviceType)) != AV_HWDEVICE_TYPE_NONE)
	{
		std::string strDeviceTypeName = av_hwdevice_get_type_name(eDeviceType);

		std::string strDeviceType = std::string("Hard device: ") + strDeviceTypeName;

		LOG_INFO_EX(strDeviceType);

		vDeviceTable.push_back(strDeviceTypeName);
	}

	if (vDeviceTable.empty())
	{
		LOG_ERROR_EX("No any hard device available for decoding !");

		ReleaseDecoder();

		return false;
	}

	// Find a special decode type from the device table
	std::string strDeviceTypeName = HARD_DEVICE_TYPE_NAME;

	std::vector<std::string>::iterator Iter = vDeviceTable.end();

	Iter = std::find_if(vDeviceTable.begin(), vDeviceTable.end(), [strDeviceTypeName](std::string strDeviceType) {

		if (strDeviceType == strDeviceTypeName)
		{
			return true;
		}

		return false;
	});

	if (Iter == vDeviceTable.end())
	{
		LOG_ERROR_EX("There is no hard device available for the decoding in device list !");

		ReleaseDecoder();

		return false;
	}

	// Get device type by name
	eDeviceType = av_hwdevice_find_type_by_name(strDeviceTypeName.c_str());

	// Get pixel format
	for (int index = 0;; index++)
	{
		const AVCodecHWConfig* pAvHardConfig = avcodec_get_hw_config(pDecoder, index);

		if (!pAvHardConfig)
		{
			std::string strErrorText = std::string("Decoder: ")
				+ pDecoder->name
				+ " does not support device type "
				+ av_hwdevice_get_type_name(eDeviceType)
				+ "\n";

			LOG_INFO_EX(strErrorText);

			ReleaseDecoder();

			return false;
		}

		if (pAvHardConfig->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX
			&& pAvHardConfig->device_type == eDeviceType)
		{
			pAvctx->pix_fmt = pAvHardConfig->pix_fmt;

			ReleaseDecoder();

			return true;
		}
	}

	ReleaseDecoder();

	return false;
}

// Init the hard device
bool VideoHardDecoder::InitHardDevice(AVCodecContext* pAvctx, AVCodec* pDecoder)
{
	if (pAvctx == NULL || pDecoder == NULL)
	{
		LOG_ERROR_EX("Decoder context is null or decoder is null !");

		return false;
	}

	// Get all available device table
	AVHWDeviceType eDeviceType = AV_HWDEVICE_TYPE_NONE;

	std::vector<std::string> vDeviceTable;

	while ((eDeviceType = av_hwdevice_iterate_types(eDeviceType)) != AV_HWDEVICE_TYPE_NONE) 
	{
		std::string strDeviceTypeName = av_hwdevice_get_type_name(eDeviceType);

		std::string strDeviceType = std::string("Hard device: ") + strDeviceTypeName;

		LOG_INFO_EX(strDeviceType);
		
		vDeviceTable.push_back(strDeviceTypeName);
	}

	if (vDeviceTable.empty())
	{
		LOG_ERROR_EX("No any hard device available for decoding !");

		return false;
	}

	// Find a special decode type from the device table
	std::string strDeviceTypeName = HARD_DEVICE_TYPE_NAME;

	std::vector<std::string>::iterator Iter = vDeviceTable.end();

	Iter = std::find_if(vDeviceTable.begin(), vDeviceTable.end(), [strDeviceTypeName](std::string strDeviceType) {
		
			if (strDeviceType == strDeviceTypeName)
			{
				return true;
			}

			return false;
	});

	if (Iter == vDeviceTable.end())
	{
		LOG_ERROR_EX("There is no hard device available for the decoding in device list !");

		return false;
	}

	// Get device type by name
	eDeviceType = av_hwdevice_find_type_by_name(strDeviceTypeName.c_str());

	// Get pixel format
	for (int index = 0;; index++) 
	{
		const AVCodecHWConfig* pAvHardConfig = avcodec_get_hw_config(pDecoder, index);

		if (!pAvHardConfig) 
		{
			std::string strErrorText = std::string("Decoder: ")
				+ pDecoder->name
				+ " does not support device type "
				+ av_hwdevice_get_type_name(eDeviceType)
				+ "\n";

			LOG_INFO_EX(strErrorText);

			return false;
		}

		if (pAvHardConfig->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX 
			&& pAvHardConfig->device_type == eDeviceType)
		{
			SetPixFmt(pAvHardConfig->pix_fmt);

			pAvctx->pix_fmt = pAvHardConfig->pix_fmt;

			break;
		}
	}

	// Create hard device context
	int iRet = av_hwdevice_ctx_create(&m_phwDevCtx, eDeviceType, NULL, NULL, 0);

	if (iRet < 0)
	{
		LOG_ERROR_EX("Failed to create specified HW device.\n");
	}

	pAvctx->hw_device_ctx = av_buffer_ref(m_phwDevCtx);

	return true;
}

// Init the decoder
bool VideoHardDecoder::Init(VideoTypeEnum eVideoType, void* pWindowHandle)
{
	if (GetIsHaveInitDecoder())
	{
		return true;
	}

	SetWinHandle(pWindowHandle);

	// Find a decoder now
	AVCodec* pDecoder = NULL;

	if (eVideoType == H264_VIDEO)
	{
		pDecoder = avcodec_find_decoder(AV_CODEC_ID_H264);
	}
	else if (eVideoType == H265_VIDEO)
	{
		pDecoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
	}

	if (pDecoder == NULL)
	{
		std::string strErrorMsg = std::string("Failed to find ")
			+ av_get_media_type_string(AVMEDIA_TYPE_VIDEO)
			+ " codec\n";

		LOG_ERROR_EX(strErrorMsg);

		return false;
	}

	// Create decode context
	AVCodecContext* pAvctx = avcodec_alloc_context3(pDecoder);

	pAvctx->flags |= AV_CODEC_FLAG_LOW_DELAY;

	pAvctx->thread_count = 0;

	SetDecCodecCtx(pAvctx);

	// Init hard device
	if (!InitHardDevice(pAvctx,pDecoder))
	{
		ReleaseDecoder();

		return false;
	}

	// Open the decode context
	if (avcodec_open2(pAvctx, pDecoder, NULL) < 0)
	{
		std::string strErrorMsg = std::string("Failed to open ")
			+ av_get_media_type_string(AVMEDIA_TYPE_VIDEO)
			+ " codec\n";

		LOG_ERROR_EX(strErrorMsg);

		ReleaseDecoder();

		return false;
	}

	// Create video render
	CreateRender();

	// Init the video render
	if (!InitVideoRender())
	{
		LOG_ERROR_EX("Failed to init video render!");

		ReleaseDecoder();

		return false;
	}

	SetIsHaveInitDecoder(true);

	return true;
}

// Render the data by GPU
bool VideoHardDecoder::RenderByGpu(AVFrame* pFrame)
{
	if (pFrame == NULL)
	{
		return false;
	}

	if (GetVideoRender() == NULL)
	{
		return false;
	}

	if (!GetVideoRender()->Render(pFrame))
	{
		return false;
	}

	return true;
}

// Decode package
bool VideoHardDecoder::DecodePacket(AVCodecContext* pAvctx,
	AVPacket* pPacket,
	uint8_t* pDst,
	int& iDstSize)
{
	if (pAvctx == NULL)
	{
		LOG_ERROR_EX("No decode context available now!");

		return false;
	}

	if(pDst == NULL)
	{
		LOG_ERROR_EX("Output buffer is null,please alloc it at first !");

		return false;
	}

	// When the packet is null which means flush the device now!!
	int iRet = avcodec_send_packet(pAvctx, pPacket);

	if (iRet < 0)
	{
		LOG_ERROR_EX("Error happed when in decoding!");

		return false;
	}

	// Decode the package here
	AVFrame* pHardFrame = NULL;

	AVFrame* pCpuFrame = NULL;

	auto DestoryFrame = [](AVFrame*& pFrame) {
	
		if (pFrame)
		{
			av_frame_free(&pFrame);

			pFrame = NULL;
		}
	};

	while (iRet >= 0)
	{
		// Alloc the frame
		if ((pHardFrame = av_frame_alloc()) == NULL
			|| (pCpuFrame = av_frame_alloc()) == NULL)
		{
			LOG_ERROR_EX("Cannot alloc frame in frame decoding loop !");

			iRet = AVERROR(ENOMEM);

			return false;
		}

		// Decode the frame now
		iRet = avcodec_receive_frame(pAvctx, pHardFrame);

		if (iRet == AVERROR(EAGAIN))
		{
			LOG_DEBUG_EX("Not ready for a frame output,need next packet in");

			DestoryFrame(pHardFrame);

			DestoryFrame(pCpuFrame);

			return false;
		}
		else if (iRet == AVERROR_EOF)
		{
			LOG_DEBUG_EX("All frames are flush!");

			DestoryFrame(pHardFrame);

			DestoryFrame(pCpuFrame);

			return true;
		}
		else if (iRet < 0)
		{
			DestoryFrame(pHardFrame);

			DestoryFrame(pCpuFrame);

			return false;
		}

		// Copy the hard frame to cpu frame if using hard devicing decoding
		AVFrame* pFinalFrame = NULL;

		if (pHardFrame->format == GetPixFmt())
		{
			if (!IsShowByGpu())
			{
				// Copy GPU data to CPU memory(It is not very good)
				if ((iRet = av_hwframe_transfer_data(pCpuFrame, pHardFrame, 0)) < 0)
				{
					LOG_ERROR_EX("Error transferring the data to system memory");

					DestoryFrame(pHardFrame);

					DestoryFrame(pCpuFrame);

					return false;
				}

				pFinalFrame = pCpuFrame;
			}
			else
			{
				// Show the data by GPU
				if (!RenderByGpu(pHardFrame))
				{
					DestoryFrame(pHardFrame);

					DestoryFrame(pCpuFrame);

					return false;
				}

				DestoryFrame(pHardFrame);

				DestoryFrame(pCpuFrame);

				return true;
			}
		}
		else
		{
			// Soft decode
			pFinalFrame = pHardFrame;
		}

		// Get the frame size
		int iFrameSize = av_image_get_buffer_size((AVPixelFormat)pFinalFrame->format, 
			pFinalFrame->width,
			pFinalFrame->height, 
			pFinalFrame->format);

		iDstSize = iFrameSize;

		// Copy the frame to dest
		iRet = av_image_copy_to_buffer(pDst, 
			iDstSize,
			(const uint8_t * const*)pFinalFrame->data,
			(const int*)pFinalFrame->linesize,
			(AVPixelFormat)pFinalFrame->format,
			pFinalFrame->width, 
			pFinalFrame->height, 
			pFinalFrame->format);

		if (iRet < 0)
		{
			LOG_ERROR_EX("Can not copy image to buffer at the last");

			DestoryFrame(pHardFrame);

			DestoryFrame(pCpuFrame);

			return false;
		}
	}

	return true;
}

// Excute the decoding
bool VideoHardDecoder::Excute(uint8_t* pSrcData,
	int iSrcSize,
	uint8_t* pDst,
	int& iDstSize,
	int& iFrameWidth,
	int& iFrameHeight)
{
	if (pSrcData == NULL)
	{
		LOG_ERROR_EX("Source data is null !");

		return false;
	}

	if (iSrcSize <= 0)
	{
		LOG_ERROR_EX("Source data size <= 0 !");

		return false;
	}

	if (pDst == NULL)
	{
		LOG_ERROR_EX("Dest buffer is null,you must alloc dest buffer at first !");

		return false;
	}

	AVCodecContext* pAvctx = GetDecCodecCtx();

	if (pAvctx == NULL)
	{
		LOG_ERROR_EX("You must init the decoder at first !");

		return false;
	}

	AVPacket CurDecPkt;

	av_init_packet(&CurDecPkt);
	
	CurDecPkt.data = pSrcData;
	
	CurDecPkt.size = iSrcSize;

	if (!DecodePacket(pAvctx, &CurDecPkt, pDst, iDstSize))
	{
		av_packet_unref(&CurDecPkt);

		return false;
	}

	av_packet_unref(&CurDecPkt);

	iFrameWidth = pAvctx->width;

	iFrameHeight = pAvctx->height;

	return true;
}
